/*
 * Copyright (c) 2012, Marcin Czekaj
 * All rights reserved.
 *
 * This software is open-source under the BSD license; see LICENSE.TXT for a description.
 */
package pl.edu.uj.fais.controller;

import static javax.swing.JOptionPane.showConfirmDialog;

import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

import javax.swing.JOptionPane;

import pl.edu.uj.fais.binding.KeyBinding;
import pl.edu.uj.fais.binding.MouseBinding;
import pl.edu.uj.fais.graph.GraphStyle;
import pl.edu.uj.fais.graph.elements.Link;
import pl.edu.uj.fais.graph.elements.Node;
import pl.edu.uj.fais.graph.io.GraphMLFileChooser;
import pl.edu.uj.fais.graph.io.HyperGraphReader;
import pl.edu.uj.fais.graph.io.HyperGraphWriter;
import pl.edu.uj.fais.gui.GraphCanvas;
import pl.edu.uj.fais.gui.MainMenu;
import pl.edu.uj.fais.gui.MainWindow;
import pl.edu.uj.fais.mode.GraphMode;
import pl.edu.uj.fais.mode.MouseMode;
import edu.uci.ics.jung.algorithms.layout.CircleLayout;
import edu.uci.ics.jung.graph.DirectedSparseGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
import edu.uci.ics.jung.io.GraphIOException;
import edu.uci.ics.jung.visualization.control.CrossoverScalingControl;
import edu.uci.ics.jung.visualization.control.ScalingControl;
import edu.uci.ics.jung.visualization.picking.PickedState;

//TODO Refactor to smaller classes
public class Controller {

	private static Controller instance;

	private MainWindow mainWindow;
	private MainMenu mainMenu;

	private Graph<Node, Link> graph;
	private CircleLayout<Node, Link> layout;
	private GraphCanvas canvas;

	private GraphMode graphMode;
	private MouseMode mouseMode;

	private ScalingControl scaler;

	private File file;
	private boolean isChanged;

	private Boolean includeData;

	public static Controller getInstance() {

		if (instance == null) {
			instance = new Controller();
		}

		return instance;
	}

	private Controller() {
		setGraphMode(GraphMode.NONE);
		scaler = new CrossoverScalingControl();
	}

	public void setMainWindow(MainWindow mainWindow) {
		this.mainWindow = mainWindow;
	}

	public void setMainMenu(MainMenu mainMenu) {
		this.mainMenu = mainMenu;
	}

	public void setMouseMode(MouseMode mode) {

		mouseMode = mode;

		if (canvas != null) {
			MouseBinding binding = MouseBinding.getInstance();
			binding.bind(this, canvas);
		}
	}

	public void setChanged() {
		isChanged = true;
	}

	public MouseMode getMouseMode() {
		return mouseMode;
	}

	public GraphMode getGraphMode() {
		return graphMode;
	}

	public GraphCanvas getGraphCanvas() {
		return canvas;
	}

	public Graph<Node, Link> getGraph() {
		return graph;
	}

	public ScalingControl getScaler() {
		return scaler;
	}

	public CircleLayout<Node, Link> getLayout() {
		return layout;
	}

	public void newDirectedGraph() {
		newGraph(new DirectedSparseGraph<Node, Link>(), GraphMode.DIRECTED);
	}

	public void newUndirectedGraph() {
		newGraph(new UndirectedSparseGraph<Node, Link>(), GraphMode.UNDIRECTED);
	}

	public void newHyperGraph() {
		newGraph(new UndirectedSparseGraph<Node, Link>(), GraphMode.HYPERGRAPH);
	}

	public void open() {
		if (graphMode != GraphMode.NONE) {
			if (close()) {
				loadGraphFromFile();
			}
		} else {
			loadGraphFromFile();
		}
	}

	public boolean close() {

		if (graphMode != GraphMode.NONE) {

			if (isChanged) {
				String message = "Do you want to save changes?";
				switch (showConfirmDialog(mainWindow, message)) {
				case JOptionPane.OK_OPTION:
					save();
					removeGraphCanvas();
					break;
				case JOptionPane.NO_OPTION:
					removeGraphCanvas();
					break;
				default:
					return false;
				}
			} else {
				removeGraphCanvas();
			}
		}

		return true;
	}

	public void save() {
		if (file != null) {
			saveGraphToFile(file);
		} else {
			saveAs();
		}
	}

	public void saveAs() {

		File file = GraphMLFileChooser.showSaveDialog();

		if (file != null) {
			includeData = null;
			
			if (saveGraphToFile(file)) {
				mainWindow.setWindowTitle(file);
				this.file = file;
			}
		}
	}
	
	public void exit() {
		if (graphMode != GraphMode.NONE) {
			if (isChanged) {
				if (close()) {
					System.exit(0);
				}
			} else {
				System.exit(0);
			}
		} else {
			System.exit(0);
		}
	}

	public void deleteSelected() {
		SelectionController.deleteSelected();
	}

	public void selectAll() {
		SelectionController.selectAll();
	}

	public void unselectAll() {
		SelectionController.unselectAll();
	}

	public void selectHyperEdge(Node node) {
		SelectionController.selectHyperEdge(node);
	}

	public void unselectHyperEdge(Node node) {
		SelectionController.unselectHyperEdge(node);
	}

	public void centerProxyNodes() {
		ProxyController.centerProxyNodes();
	}

	public void centerProxyNode(Node proxy, Node start, Node end) {
		ProxyController.centerProxyNode(proxy, start, end);
	}

	public void editLabel() {

		if (SelectionController.isSingleNodeSelected()) {

			PickedState<Node> selectedNodes = canvas.getPickedVertexState();
			Node node = (Node) selectedNodes.getSelectedObjects()[0];

			String oldLabel = node.getLabel();

			String message = String.format("Label of Node#%d:", node.getId());
			String newLabel = JOptionPane.showInputDialog(message, oldLabel);

			if ("".equals(newLabel)) {
				newLabel = null;
			}

			node.setLabel(newLabel);

			if (hasTextChanged(oldLabel, newLabel)) {
				canvas.repaint();
				isChanged = true;
			}
		}
	}

	public void toggleLabels() {
		GraphStyle.toggleLabels(canvas);
		canvas.repaint();
	}

	public void zoomIn() {
		scaler.scale(canvas, GraphStyle.ZOOM_IN_FACTOR, mainWindow.getCenter());
	}

	public void zoomOut() {
		scaler.scale(canvas, GraphStyle.ZOOM_OUT_FACTOR, mainWindow.getCenter());
	}

	public void reset() {

		GraphMode mode = graphMode;

		// make backup copy
		File file = this.file;
		boolean isChanged = this.isChanged;

		removeGraphCanvas();
		addGraphCanvas(graph);

		// restore
		this.file = file;
		this.isChanged = isChanged;
		mainWindow.setWindowTitle(file);

		updateModelNodePositions();

		setGraphMode(mode);
	}

	private void setGraphMode(GraphMode mode) {

		graphMode = mode;

		updateMainWindow();
		updateMainMenu();
		bindKeys();
	}

	private void updateMainWindow() {
		if (graphMode == GraphMode.NONE) {

			isChanged = false;
			file = null;

			if (mainWindow != null) {
				mainWindow.setEmptyWindowTitle();
			}
		}
	}

	private void updateMainMenu() {
		if (mainMenu != null) {
			mainMenu.updateState(graphMode);
		}
	}

	private void bindKeys() {

		KeyBinding binding = KeyBinding.getInstance();

		if (graphMode == GraphMode.NONE) {
			binding.unbind();
		} else {
			binding.rebind(canvas);
		}
	}

	private void newGraph(Graph<Node, Link> graph, GraphMode mode) {
		if (close()) {
			initNewGraphState();
			addGraphCanvas(graph);
			setGraphMode(mode);
		}
	}

	private void initNewGraphState() {
		isChanged = true;
		file = null;
		mainWindow.setUntitledWindowTitle();
		Node.resetCounter();
	}

	private void addGraphCanvas(Graph<Node, Link> graph) {

		this.graph = graph;

		layout = getLayout(graph);
		canvas = new GraphCanvas(layout);

		canvas.setSize(mainWindow.getClientSize());
		canvas.setDoubleBuffered(true);
		
		mainWindow.add(canvas);

		setMouseMode(MouseMode.EDIT);
	}

	private void removeGraphCanvas() {

		mainWindow.remove(canvas);
		mainWindow.repaint();

		setGraphMode(GraphMode.NONE);
	}

	private CircleLayout<Node, Link> getLayout(Graph<Node, Link> graph) {

		CircleLayout<Node, Link> layout = new CircleLayout<>(graph);
		layout.setSize(mainWindow.getClientSize(100));

		return layout;
	}

	private boolean hasTextChanged(String a, String b) {
		if (b == null) {
			return a != null;
		} else {
			return !b.equals(a);
		}
	}

	private boolean saveGraphToFile(File file) {
		if (includeData == null) {
			switch (GraphMLFileChooser.showIncludeDataDialog()) {
			case JOptionPane.CANCEL_OPTION:
				return false;
			case JOptionPane.YES_OPTION:
				includeData = true;
				break;
			default:
				includeData = false;
			}
		}
		
		if (includeData) {
			updateModelNodePositions();
		}
		
		try {
			HyperGraphWriter writer = new HyperGraphWriter(file);
			writer.save(graph, graphMode, includeData);
			isChanged = false;
		} catch (IOException ex) {
			mainWindow.showErrorMessage(ex.getMessage());
			return false;
		}
		
		return true;
	}

	private void loadGraphFromFile() {

		File file = GraphMLFileChooser.showOpenDialog();

		if (file != null) {

			try {
				this.file = file;
				isChanged = false;

				mainWindow.setWindowTitle(file);
				Node.resetCounter();

				HyperGraphReader reader = new HyperGraphReader(file);
				addGraphCanvas(reader.getGraph());
				setGraphMode(reader.getGraphMode());

				updateLayoutNodePositions();

			} catch (FileNotFoundException | GraphIOException ex) {
				mainWindow.showErrorMessage(ex.getMessage());
				initNewGraphState();
				close();
			}
		}
	}

	private void updateModelNodePositions() {
		for (Node node : graph.getVertices()) {

			double x = layout.getX(node);
			double y = layout.getY(node);

			node.setPosition(x, y);
		}
	}

	private void updateLayoutNodePositions() {

		for (Node node : graph.getVertices()) {

			double x = node.getX();
			double y = node.getY();

			layout.setLocation(node, new Point2D.Double(x, y));
		}

		ProxyController.centerProxyNodes();
	}

	public void showHelp() {
		String helpText = "<html><body>" + 
				"<h4>Mouse / Keyboard</h4>" + 
				"<ul>" + 
				"<li><code>hold [Ctrl]</code>, edit mouse mode (click to add new node, drag to connect nodes)</li>" + 
				"<li><code>hold [Shift]</code>, view mouse mode (drag to translate, wheel to zoom)</li>" + 
				"<li><code>hold [Spacebar]</code>, select hyperedge component (only in hypergraph mode)</li>" + 
				"<li><code>[Del]</code>, delete selected elements</li>" + 
				"<li><code>[F2]</code>, edit label of the selected node</li>" + 
				"<li><code>[Ctrl] + [A]</code>, select all nodes and (hyper)edges</li>" + 
				"<li><code>[Ctrl] + [S]</code>, save current graph to file</li>" + 
				"<li><code>[Ctrl] + [W]</code>, close current graph</li>" + 
				"</ul>" + 
				"</body></html>";
		JOptionPane.showMessageDialog(mainWindow, helpText, "Help", JOptionPane.PLAIN_MESSAGE);

	}
}
